/*
 *  Copyright 2012 Zdeněk Janeček

 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at

 *    http://www.apache.org/licenses/LICENSE-2.0

 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package kiv.pt;

import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

import kiv.pt.ui.IniTrida;

/**
 * Řídí výkony elektráren na základě informací z aktuálního odběru.
 * 
 * @author Zdeněk Janeček
 */
public class PowController {
    /**
     * Ztrátový koeficient. Přenásobením dostanete množství energie,
     * která projde vodičem.
     */
    public static final double LOST = 0.9975f;

    private static final int NEW_FLAG = 1;
    private static final int CLOSED_FLAG = 2;
    private static final double INFINITY = 500f;

    /**
     * Maximální výkon hlavní elektrárny
     */
    public static final int HL_MAX = 150000;

    /**
     * Maximální výkon alternativní elektrárny
     */
    public static final int ALT_MAX = 10000;

    /**
     * Cena za kWh hlavního zdroje.
     */
    public static final double PRICE_MAIN = 1.8f;

    /**
     * Cena za kWh alternativního zdroje.
     */
    public static final double PRICE_ALT = 2f;

    /**
     * Pole dodavatelů energie pro každé odběrné místo.
     */
    public int[] suppliers = new int[DataGuru.mista.size()];

    /**
     * Pole vzdáleností k odběrnému místu ze zdroje, který konkrétně využívá.
     * Zjistitelné v poli {@code suppliers}.
     */
    public double[] dists = new double[DataGuru.mista.size()];

    /**
     * Pole zátěží jednotlivých zdrojů.
     */
    public double[] zateze = new double[DataGuru.zdroje.size()];
    
    /**
     * Množství celkově spotřebované energie v každém místě.
     */
    public double[] mistaCelk = new double[DataGuru.mista.size()];
    
    /**
     * Kolik celkově zaplatilo každé odběrné místo.
     */
    public double[] cenaCelk = new double[DataGuru.mista.size()];
    
    /**
     * Nemá velký význam. Používá se jen na volání metod. Jedná se o služebníka.
     */
    public PowController() {
        // není třeba nic inicializovat.
    }
    
    /**
     * Slouží jako jednoduchá prioritní fronta. Odstraní z ní nejbližší vrchol
     * podle dodaného pole vzdáleností.
     * 
     * @param fronta
     *            Fronta k zpracování
     * @param dists
     *            Distanční pole
     * @return id nalezeného vrcholu
     */
    private int nejbVpred(List<Integer> fronta, double[] dists) {
        double minDistance = Double.MAX_VALUE;
        int iter = 0;
        int nodeIn = 0;
        for (Integer i : fronta) {
            if (dists[i] < minDistance) {
                minDistance = dists[i];
                nodeIn = iter;
            }
            iter++;
        }

        return fronta.remove(nodeIn);
    }

    /**
     * Spočte zátěže a přiřadí zdroje. Spustit na začátku programu.
     */
    public void calcNetworkStart() {
        if (dists.length < DataGuru.mista.size()) {
            dists = new double[DataGuru.mista.size()];
        } else if (suppliers.length < DataGuru.mista.size()) {
            suppliers = new int[DataGuru.mista.size()];
        }
        // initialization
        Arrays.fill(dists, INFINITY);
        Arrays.fill(suppliers, -1);

        for (OdberneMisto m : DataGuru.mista) {
            Arrays.fill(m.powDists, Double.POSITIVE_INFINITY);
            Arrays.fill(m.powSuppliers, -1);
        }

        int[] flags = new int[DataGuru.mista.size()];

        for (ZdrojEnergie ze : DataGuru.zdroje) {
            // text.changeScreen("Počkejte prosím",
            // "Pracuji... zdroj "+ze.getId());

            List<Integer> fronta = new LinkedList<Integer>();
            Arrays.fill(flags, NEW_FLAG);

            Integer[] sousedi = ze.seznamSousedu();

            for (Integer n : sousedi) {
                double d = DataGuru.distanceFromTable(true, ze.getId(), n);

                OdberneMisto m = DataGuru.mista.get(n);

                if (d < dists[n]) {
                    if (suppliers[n] > -1) {
                        m.powSuppliers[suppliers[n]] = 1;
                        m.powDists[suppliers[n]] = dists[n];
                    }
                    suppliers[n] = ze.getId();
                    dists[n] = d;
                } else {
                    m.powSuppliers[ze.getId()] = 1;
                    m.powDists[ze.getId()] = d;
                }
                fronta.add(n);
            }

            ((LinkedList<Integer>) fronta).addFirst(nejbVpred(fronta, dists));

            while (!fronta.isEmpty()) {
                // najdi nejblizsi dosazitelny uzel
                int node = nejbVpred(fronta, dists);

                flags[node] = CLOSED_FLAG;

                Integer[] uzly = DataGuru.mista.get(node).seznamSousedu();
                for (Integer uzel : uzly) { // potomci zpracovavaneho uzlu
                    double newDist = dists[node]
                            + DataGuru.distanceFromTable(false, node, uzel);

                    if (flags[uzel] == NEW_FLAG && newDist < dists[uzel]) {
                        OdberneMisto m = DataGuru.mista.get(uzel);
                        
                        if (newDist < dists[uzel]) {
                            if (suppliers[uzel] > -1) {
                                m.powSuppliers[suppliers[uzel]] = 1;
                                m.powDists[suppliers[uzel]] = dists[uzel];
                            }
                            suppliers[uzel] = ze.getId();
                            dists[uzel] = newDist;
                        } else {
                            m.powSuppliers[ze.getId()] = 1;
                            m.powDists[ze.getId()] = newDist;
                        }

                        fronta.add(uzel); // pridame ke zpracovani
                    }
                }
            }
        }
    }

    /**
     * Nastavý pole {@code zateze} na hodnoty v kWh. Každé id elektrárny odpovídá
     * pořadí v tomto poli.
     */
    public void calcZateze() {
        Arrays.fill(zateze, 0);
        for (int i = 0; i < dists.length; i++) {
            double zatez = Math.pow(2 - PowController.LOST, dists[i])
                    * DataGuru.mista.get(i).getPrikon();
            
            if (zateze[suppliers[i]] + zatez < DataGuru.zdroje
                    .get(suppliers[i]).getVykon()) {

                zateze[suppliers[i]] += zatez;   
                continue;
            }
            
            boolean nalezeno = false;
            int index = 0;
            double minPrice = Double.MAX_VALUE;
            OdberneMisto m = DataGuru.mista.get(i);
            
            for (int alt = 0; alt < m.powSuppliers.length; alt++) {
                if (m.powSuppliers[alt] == -1) {
                    continue;
                }
                
                double price = alt == 0 ? PRICE_MAIN : PRICE_ALT;
                double altZatez = Math.pow(2 - PowController.LOST,
                        m.powDists[alt]) * m.getPrikon();
                double vykon = DataGuru.zdroje.get(alt).getVykon();

                if (altZatez * price < minPrice && zateze[alt] + altZatez < vykon) {
                    index = alt;
                    minPrice = altZatez * price;
                    nalezeno = true;
                }
            }
            
            if (nalezeno) {
                zateze[index] += Math.pow(2 - PowController.LOST,
                        m.powDists[index]) * DataGuru.mista.get(i).getPrikon();
                suppliers[i] = index;
                dists[i] = m.powDists[index];
            } else {
                IniTrida.errorMessage("místo "+i+" nelze napájet\n");
            }
        }
    }

    /**
     * Možné spustit po každém {@code calcNetworkStart()}. Vypíše ke každému
     * odběrnému místu seznam zdrojů, ze kterých bylo napájeno.
     */
    public void printSuppliers() {
        for (int i = 0; i < suppliers.length; i++) {
            System.out.format("dodavatel místa %d: ", i);
            if (suppliers[i] == -1) {
                System.out.println("N/A");
            } else {
                System.out.println(suppliers[i]);
            }
            System.out.format("  výkon/příkon: %d/%.2f\n",
                    DataGuru.zdroje.get(suppliers[i]).getVykon(),
                    DataGuru.mista.get(i).getPrikon());
        }
    }

    /**
     * Možné spustit po každém {@code calcNetworkStart()}. Vypíše cenu za přenos
     * ke každému místu.
     */
    public void printPrices() {
        double price;
        double trafLost;
        for (int i = 0; i < dists.length; i++) {
            price = suppliers[i] == 0 ? PRICE_MAIN : PRICE_ALT;
            trafLost = Math.pow(1 - LOST, dists[i])
                    * DataGuru.mista.get(i).getPrikon();
            System.out.format("misto(%d) z=%d cena=%f\n", i, suppliers[i],
                    trafLost * price);
        }
    }

    /**
     * Přizpůsobí výkon zdrojů podle aktuální spotřeby.
     * 
     * @param sim Simulační třída, ze které dostane informace o  budoucích
     * příkonech.
     */
    public void regulujVykon(Simulator sim) {
        double[] futZatez = new double[DataGuru.zdroje.size()];
        
        for (int i = 0; i < suppliers.length; i++) {
            futZatez[suppliers[i]] += Math.pow(2 - PowController.LOST, dists[i]) *
                    sim.futurePrikon[i];
        }
        
        double vykon = DataGuru.zdroje.get(0).getVykon();
        double deltaMain = (futZatez[0] * 1.09) - vykon;
        
        /*
        if (Math.abs(deltaMain) < (vykon * 0.1)) {
            DataGuru.zdroje.get(0).setVykon((int) (futZatez[0] * 1.09));
        } else if (deltaMain >= 0) {
            DataGuru.zdroje.get(0).setVykon((int) (vykon * 1.1));
        } else {
            DataGuru.zdroje.get(0).setVykon((int) (vykon * 0.9));
        }
        */
        for (int i = 0; i < futZatez.length; i++) {
            DataGuru.zdroje.get(i).setVykon((int) (futZatez[i] * 1.08));
        }
    }
}
